package com.pangus.toolkits.files.download;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileMode;
import com.pangus.toolkits.files.errors.uncheck.FSException;
import com.pangus.toolkits.files.model.FileInfo;
import com.pangus.toolkits.files.operation.FSFileOperation;
import com.pangus.toolkits.files.request.Range;

import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CompletableFuture;

/**
 * @author yingheng chen
 * @description
 * @date 2019/12/5
 */
public class ResumePartDownload implements DownloadStrategy {
    private final FileInfo fileInfo;
    private final FSFileOperation fileOperation;
    private static final int CONCURRENCY = 4;
    private static final String CP_SUFFIX = ".cp";
    private File tempFile;
    private File cpFile;

    public ResumePartDownload(FileInfo fileInfo, FSFileOperation fileOperation) {
        this.fileInfo = fileInfo;
        this.fileOperation = fileOperation;

    }

    /**
     * 1.读取上次的下载结果checkpoint，如果checkpoint不存在，或者文件被删除,重新下载
     * 2.根据check point 下载，写入到tmp ( check crc64?)
     * 3.检查下载分片是否有异常
     * 4.tmp文件改名
     * 5.删除check point
     *
     * @param parent
     * @param filename
     * @return
     * @throws IOException
     */
    @Override
    public File download(@Nonnull File parent, String filename) throws IOException {
        this.tempFile = FileUtil.file(parent, filename + ".tmp");
        this.cpFile = FileUtil.file(parent, filename + CP_SUFFIX);
        DownloadCheckPoint downloadCheckPoint = new DownloadCheckPoint();
        try {
            downloadCheckPoint.load(cpFile);
        } catch (Exception e) {
            removeCheckpoint(cpFile);
        }
        if (!downloadCheckPoint.isValid(this.fileInfo)) {
            prepare(downloadCheckPoint, parent, filename);
            removeCheckpoint(cpFile);
        }
        //根据check point 去下载
        List<PartResult> downloadRes = downloadByCheckPoint(downloadCheckPoint);
        for (PartResult part : downloadRes) {
            if (part.isFailed()) {
                throw new FSException(part.getException());
            }
        }

        final File result = FileUtil.rename(this.tempFile, filename, false, true);
        removeCheckpoint(cpFile);
        return result;
    }

    private List<PartResult> downloadByCheckPoint(DownloadCheckPoint downloadCheckPoint) {
        ArrayList<PartResult> taskResults = new ArrayList<>();
        ArrayList<CompletableFuture<PartResult>> futures = new ArrayList<>();
        for (int i = 0; i < downloadCheckPoint.downloadParts.size(); i++) {
            final DownloadPart part = downloadCheckPoint.downloadParts.get(i);
            if (!part.isCompleted) {
                final DownLoadTask downLoadTask = DownLoadTask.builder()
                        .id(i)
                        .partIndex(i)
                        .fileInfo(fileInfo)
                        .downloadCheckPoint(downloadCheckPoint)
                        .fileOperation(fileOperation)
                        .name("downloading-" + fileInfo.getName() + "-" + i)
                        .tempFile(tempFile)
                        .cpFile(cpFile)
                        .build();
                final CompletableFuture<PartResult> taskFuture = CompletableFuture.supplyAsync(downLoadTask);
                futures.add(taskFuture);
            } else {
                taskResults.add(new PartResult(i + 1, part.start, part.end, part.length));
            }
        }
        //wait
        CompletableFuture allOf = CompletableFuture
                .allOf(futures.toArray(new CompletableFuture[futures.size()]));
        allOf.join();
        taskResults.sort(Comparator.comparingInt(PartResult::getNumber));
        return taskResults;
    }

    private void prepare(DownloadCheckPoint downloadCheckPoint, @Nonnull File parent, String filename) throws IOException {
        downloadCheckPoint.magic = DownloadCheckPoint.DOWNLOAD_MAGIC;
        downloadCheckPoint.downloadFile = Paths.get(parent.getPath(), filename).toString();
        ObjectStat objectStat = new ObjectStat();
        objectStat.size = this.fileInfo.getSize();
        objectStat.lastModified = this.fileInfo.getLastModified();
        downloadCheckPoint.objectStat = objectStat;
        downloadCheckPoint.downloadParts = splitFile();
        try (RandomAccessFile tempFile = FileUtil.createRandomAccessFile(this.tempFile, FileMode.rw)) {
            tempFile.setLength(fileInfo.getSize());
        }
    }

    private ArrayList<DownloadPart> splitFile() {
        ArrayList<DownloadPart> parts = new ArrayList<>();
        long fileSize = fileInfo.getSize();
        long partSize = fileSize / CONCURRENCY;
        long offset = 0L;
        for (int i = 0; offset < fileSize; offset += partSize, i++) {
            DownloadPart part = new DownloadPart();
            part.index = i;
            part.start = offset;
            part.end = getPartEnd(offset, fileSize, partSize);
            part.length = partSize;
            parts.add(part);
        }
        return parts;
    }

    private long getPartEnd(long begin, long total, long per) {
        if (begin + per > total) {
            return total - 1;
        }
        return begin + per - 1;
    }

    private boolean removeCheckpoint(File cpFile) {
        boolean flag = false;
        if (cpFile.isFile() && cpFile.exists()) {
            flag = cpFile.delete();
        }
        return false;
    }

    private List<Range> splitRange(Long size) {
        Long partSize = size / CONCURRENCY;
        final ArrayList<Range> ranges = new ArrayList<>(4);
        Long begin = 0L;
        long end = partSize - 1;
        for (int i = 0; i < CONCURRENCY; i++) {
            ranges.add(new Range(begin, end));
            begin = end + 1;
            end = begin + partSize - 1;
        }
        return ranges;
    }

}
